﻿using DotNetty.Buffers;
using DotNetty.Codecs.Http;
using DotNetty.Codecs.Http.WebSockets;
using DotNetty.Handlers.Tls;
using DotNetty.Transport.Bootstrapping;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Sockets;

namespace DotNetty.Extensions
{
    public class WebSocketClient : IAsyncDisposable
    {
        public WebSocketClient(string uri, int timeout = 3)
        {
            _uri = new Uri(uri);
            _timeout = timeout;
        }

        private readonly Uri _uri;

        private readonly int _timeout;

        public int MaxFramePayloadLength { get; set; } = 5 * 1024 * 1024;

        public bool UseSsl { get; internal set; }

        public bool TcpNodelay { get; set; } = true;

        public int WorkerGroupCount { get; set; } = 0;

        private IEventLoopGroup group;

        public IChannel Channel { get; internal set; }

        private readonly SemaphoreSlim _slim = new(1, 1);

        private bool _slimDispose = false;

        internal readonly SemaphoreSlim _slim2 = new(0, 1);

        public event Action<Bootstrap> OnCreateBootstrap;

        public event Action<IChannelPipeline> OnChannelPipeline;

        public event Action OnConnecting;

        public event Action OnConnected;

        public event Action<string> OnTextMessage;

        public event Action<byte[]> OnBinaryMessage;

        public event Action<Exception> OnException;

        public event Action OnDisconnected;

        internal void Connected()
        {
            try
            {
                OnConnected?.Invoke();
            }
            catch { }
        }

        internal void TextMessage(string msg)
        {
            try
            {
                OnTextMessage?.Invoke(msg);
            }
            catch { }
        }

        internal void BinaryMessage(byte[] bytes)
        {
            try
            {
                OnBinaryMessage?.Invoke(bytes);
            }
            catch { }
        }

        internal void Ex(Exception ex)
        {
            try
            {
                OnException?.Invoke(ex);
            }
            catch { }
        }

        internal void Disconnected()
        {
            try
            {
                OnDisconnected?.Invoke();
            }
            catch { }
        }

        private async Task CloseGroupAsync()
        {
            if (group != null)
            {
                try
                {
                    await group.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1));
                }
                catch { }
            }
        }

        private async Task ConnectBaseAsync()
        {
            try
            {
                if (WorkerGroupCount <= 0)
                {
                    group = new MultithreadEventLoopGroup();
                }
                else
                {
                    group = new MultithreadEventLoopGroup(WorkerGroupCount);
                }

                var bootstrap = new Bootstrap()
                        .Group(group)
                        .Channel<TcpSocketChannel>()
                        .Option(ChannelOption.TcpNodelay, TcpNodelay)
                        .Option(ChannelOption.ConnectTimeout, TimeSpan.FromSeconds(_timeout))
                        .Handler(new ActionChannelInitializer<ISocketChannel>(ch =>
                        {
                            IChannelPipeline pipeline = ch.Pipeline;
                            OnChannelPipeline?.Invoke(pipeline);

                            var tls = pipeline.FirstOrDefault(f => f.GetType() == typeof(TlsHandler));
                            UseSsl = tls != null;

                            pipeline.AddLast(new HttpClientCodec());
                            pipeline.AddLast(new HttpObjectAggregator(8192));
                            var protoco = new WebSocketClientProtocolHandler(
                                _uri,
                                WebSocketVersion.V13,
                                null,
                                true,
                                new DefaultHttpHeaders(),
                                MaxFramePayloadLength,
                                true,
                                true,
                                false);

                            pipeline.AddLast(protoco);
                            pipeline.AddLast(new WebSocketClientHandler(this));

                        }));

                OnCreateBootstrap?.Invoke(bootstrap);

                try
                {
                    OnConnecting?.Invoke();
                }
                catch { }

                Channel = await bootstrap.ConnectAsync(_uri.Host, _uri.Port); //虽然连接上了服务器，但是还未握手成功

                var ok = await _slim2.WaitAsync(TimeSpan.FromSeconds(_timeout));
                if (!ok)
                {
                    try
                    {
                        await Channel.CloseAsync(); //握手失败关闭连接

                        Ex(new Exception("handshake with server timeout."));
                    }
                    catch
                    {
                        try
                        {
                            OnDisconnected?.Invoke();
                        }
                        catch { }
                    }
                    return;
                }

                try
                {
                    OnConnected?.Invoke();
                }
                catch { }

            }
            catch (Exception ex)
            {
                await CloseGroupAsync();

                try
                {
                    OnException?.Invoke(ex);
                }
                catch { }

                try
                {
                    OnDisconnected?.Invoke();
                }
                catch { }
            }
        }

        public async Task ConnectAsync()
        {
            if (_slimDispose)
            {
                return;
            }

            try
            {
                var ok = await _slim.WaitAsync(0);
                if (!ok)
                {
                    return;
                }

                await ConnectBaseAsync();

                _slim.Release();
            }
            catch { }
        }

        public async Task WriteAndFlushAsync(string text)
        {
            var frame = new TextWebSocketFrame(text);
            await Channel.WriteAndFlushAsync(frame);
        }

        public async Task WriteAndFlushAsync(IByteBuffer buffer)
        {
            var frame = new BinaryWebSocketFrame(buffer);
            await Channel.WriteAndFlushAsync(frame);
        }

        public async Task WriteAndFlushAsync(byte[] bytes)
        {
            var frame = new BinaryWebSocketFrame(Unpooled.WrappedBuffer(bytes));
            await Channel.WriteAndFlushAsync(frame);
        }

        public async ValueTask DisposeAsync()
        {
            await _slim.WaitAsync();
            _slimDispose = true;
            _slim2.Dispose();

            if (Channel != null)
            {
                try
                {
                    await Channel.CloseAsync();
                    Channel = null;
                }
                catch { }
            }

            await CloseGroupAsync();

            _slim.Release();
            _slim.Dispose();

            GC.SuppressFinalize(this);
        }
    }
}
